package edu.gatech.app4args.utils;

import edu.gatech.app4args.resources.Globals;
import edu.gatech.app4args.resources.ResourceLoader;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import org.docx4j.convert.out.flatOpcXml.FlatOpcXmlCreator;
import org.docx4j.dml.*;
import org.docx4j.jaxb.NamespacePrefixMapperUtils;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.exceptions.InvalidFormatException;
import org.docx4j.openpackaging.packages.PresentationMLPackage;
import org.docx4j.openpackaging.parts.PartName;
import org.docx4j.openpackaging.parts.PresentationML.MainPresentationPart;
import org.docx4j.openpackaging.parts.PresentationML.SlideLayoutPart;
import org.docx4j.openpackaging.parts.PresentationML.SlideMasterPart;
import org.docx4j.openpackaging.parts.PresentationML.SlidePart;
import org.docx4j.XmlUtils;

import org.pptx4j.jaxb.Context;
import org.pptx4j.pml.CTPlaceholder;
import org.pptx4j.pml.NvPr;
import org.pptx4j.pml.STPlaceholderType;
import org.pptx4j.pml.Shape;
import org.pptx4j.pml.Shape.NvSpPr;
import org.pptx4j.pml.SldLayout;

/**
 * Handles all pptx creation. Uses the docx4j & pptx4j libraries to handle
 * the writing of pptx files.
 *
 * @author Andrew Lunsford
 */
public class PptxCreator extends FileExporter {
	/** The main presentation package that handles creation of the presentation */
	private PresentationMLPackage presMLPackage;
	/** The main presentation part controls all slide, themes and layout links */
	private MainPresentationPart presMain;
	/** Keeps & adds references between slides and their settings */
	private SlideMasterPart slideMaster;
	/** The layout for a title slide */
	private SlideLayoutPart titleLayout;
	/** The layout for a title and content slide */
	private SlideLayoutPart standardLayout;
	/** The layout for a title and two content slide */
	private SlideLayoutPart twoContentLayout;
	/** The current slide number */
	private int slideNum;

	/**
	 * Creates the class, setting up the main parts necessary to write out a
	 * pptx file. Also sets up the 3 layouts used in this application.
	 */
	public PptxCreator(){
		// init objects
		slideNum=1;
		titleLayout = null;
		standardLayout = null;
		twoContentLayout = null;
		try {
			presMLPackage = PresentationMLPackage.createPackage();
			presMain = (MainPresentationPart)presMLPackage.getParts().getParts().get(new PartName("/ppt/presentation.xml"));
			slideMaster = (SlideMasterPart)presMLPackage.getParts().getParts().get(new PartName("/ppt/slideMasters/slideMaster1.xml"));
		} catch (InvalidFormatException e) {
			System.err.println("Error: PC> presMLPackage creation failed");
		}

		// create all layouts
		createTitleLayout();
		createStandardLayout();
		createTwoContentLayout();
	}

	/**
	 * A test method to use all 3 layouts and use dummy data.
	 */
	public void testPptx(){
		ArrayList<String> temp = new ArrayList<String>();

		// title slide
		SlidePart slide = createTitleSlide();

		temp.add("Testing bullets n stuff");
		this.addTitle(slide, temp);
		temp.clear();
		temp.add("the subtitles broken ill bet");
		this.addSubtitle(slide, temp);
		//*/
		// content slide
		slide = createStandardSlide();
		temp.clear();
		temp.add("bullets by default maybe?");
		this.addTitleS(slide, temp);
		temp.clear();
		temp.add("bullet 1 hopefully");
		temp.add("bullet num2");
		this.addContent1(slide, temp);

		// 2 content slide
		slide = createTwoContentSlide();
		temp.clear();
		temp.add("third slide");
		this.addTitle(slide, temp);
		temp.clear();
		temp.add("one thing");
		this.addContent1(slide, temp);
		temp.clear();
		temp.add("two thing");
		this.addContent2(slide, temp);

		// write out
		write(System.getProperty("user.dir") + "/testing_pres.pptx");
	}

	/**
	 * Loads the title layout. It is done differently because it is already
	 * created by PresentationMLPackage.createPackage().
	 */
	private void createTitleLayout(){
		try {
			//slide layout 1 already exists, get and set
			titleLayout = (SlideLayoutPart) presMLPackage.getParts().getParts().get(new PartName("/ppt/slideLayouts/slideLayout1.xml"));
			// unmarshal and set SlideLayout
			titleLayout.setJaxbElement((SldLayout) XmlUtils.unmarshalString(this.getLayoutXML("title"), Context.jcPML));

		} catch (InvalidFormatException ex) {
			System.err.println("Error: PC> SlideLayoutPart creation failed");
		} catch (JAXBException e) {
			System.err.println("Error: PC> JAXB slide layout write failed");
		}
	}

	/**
	 * Loads the standard layout, using its layout number and name.
	 */
	private void createStandardLayout(){
		standardLayout = createLayout(2, "standard");
	}

	/**
	 * Loads the two content layout, using its layout number and name.
	 */
	private void createTwoContentLayout(){
		twoContentLayout = createLayout(4, "twocontent");
	}

	/**
	 * Loads a slide layout from within the resources/slides package. Adds the
	 * layout to the slideMaster and adds a link back to the master in the
	 * layout.
	 *
	 * @param layoutNumber the layout number to set
	 * @param slideName the name of the layout to load
	 * @return the loaded slide layout
	 */
	private SlideLayoutPart createLayout(int layoutNumber, String slideName){
		SlideLayoutPart layoutPart = null;
		try {
			// create new slide layout
			layoutPart = new SlideLayoutPart(new PartName("/ppt/slideLayouts/slideLayout"+layoutNumber+".xml"));
			// read in slide layout, unmarshal, and set
			layoutPart.setJaxbElement((SldLayout) XmlUtils.unmarshalString(this.getLayoutXML(slideName), Context.jcPML));

			// add reference in slideMaster to layout and vice versa
			slideMaster.addSlideLayoutIdListEntry(layoutPart);
			layoutPart.addTargetPart(slideMaster);

		} catch (JAXBException e) {
			System.err.println("Error: PC> JAXB slide layout write failed");
		} catch (InvalidFormatException e) {
			System.err.println("Error: PC> SlideLayoutPart creation failed");
		} catch (Exception e){
			System.err.println("Error: PC> slideLayout did not pass validation");
		}
		return layoutPart;
	}

	/**
	 * Gets the layout for a slide type from the resources/slides package.
	 *
	 * @param layout the layout to load
	 * @return the layout read into a string
	 */
	private String getLayoutXML(String layout){
		String layoutXML = null;
		try {
			layoutXML = ResourceLoader.slideLayoutFile("slides/" + layout + ".xml");
		} catch (IOException ex) {
			System.err.println("Error: PC> Failed to get layout xml");
		}
		return layoutXML;
	}

	/**
	 * Creates a blank title slide.
	 *
	 * @return a SlidePart with the title layout
	 */
	private SlidePart createTitleSlide(){
		return createNewSlide(titleLayout);
	}

	/**
	 * Creates a blank standard slide.
	 *
	 * @return a SlidePart with the standard layout
	 */
	private SlidePart createStandardSlide(){
		return createNewSlide(standardLayout);
	}

	/**
	 * Creates a blank two content slide.
	 *
	 * @return a SlidePart with the two content layout
	 */
	private SlidePart createTwoContentSlide(){
		return createNewSlide(twoContentLayout);
	}

	/**
	 * Creates a new slide using the given layout.
	 *
	 * @param layout the layout to use for the slide
	 * @return a new slide with the given layout
	 */
	private SlidePart createNewSlide(SlideLayoutPart layout){
		SlidePart tempSlide = null;
		try {
			tempSlide = PresentationMLPackage.createSlidePart(presMain, layout, new PartName("/ppt/slides/slide"+slideNum+".xml"));
		}  catch (InvalidFormatException ex) {
			System.err.println("Error: PC> SlideLayoutPart creation failed");
		} catch (JAXBException e) {
			System.err.println("Error: PC> JAXB slide layout write failed");
		}
		slideNum++;
		return tempSlide;
	}

	/**
	 * Adds a title to a title slide.
	 *
	 * @param slide the slide to add to
	 * @param content the data to add to the title
	 */
	private void addTitle(SlidePart slide, ArrayList<String> content){
		// create a new shape and set its id and name
		Shape titleShape = createShape(2, "Title 1", STPlaceholderType.CTR_TITLE, 0);
		
		// create SpPr
		titleShape.setSpPr(new CTShapeProperties());
		CTTextBody ctTB = createTextBody(content);
		ctTB.getP().get(0).getPPr().setAlgn(STTextAlignType.CTR);
		//ctTP.getPPr().setAlgn(STTextAlignType.CTR);
		// add textbody
		titleShape.setTxBody(ctTB);
		// add shape
		slide.getJaxbElement().getCSld().getSpTree().getSpOrGrpSpOrGraphicFrame().add(titleShape);
	}

	/**
	 * Adds a subtitle to a title slide.
	 *
	 * @param slide the slide to add data to
	 * @param content the data to add to the subtitle
	 */
	private void addSubtitle(SlidePart slide, ArrayList<String> content){
		// create a new shape and set its id and name
		Shape titleShape = createShape(3, "Subtitle 2", STPlaceholderType.SUB_TITLE, 1);

		// create SpPr
		titleShape.setSpPr(new CTShapeProperties());
		CTTextBody ctTB = createTextBody(content);
		ctTB.getP().get(0).getPPr().setAlgn(STTextAlignType.CTR);
		//ctTP.getPPr().setAlgn(STTextAlignType.CTR);
		// add textbody
		titleShape.setTxBody(ctTB);
		// add shape
		slide.getJaxbElement().getCSld().getSpTree().getSpOrGrpSpOrGraphicFrame().add(titleShape);
	}

	/**
	 * Adds a title to a non title slide.
	 *
	 * @param slide the slide to add data to
	 * @param content the data to add to the title
	 */
	private void addTitleS(SlidePart slide, ArrayList<String> content){
		// create a new shape and set its id and name
		Shape titleShape = createShape(1, "Title 1", STPlaceholderType.TITLE, 0);

		// create SpPr
		titleShape.setSpPr(new CTShapeProperties());
		CTTextBody ctTB = createTextBody(content);
		ctTB.getP().get(0).getPPr().setAlgn(STTextAlignType.CTR);
		//ctTP.getPPr().setAlgn(STTextAlignType.CTR);
		// add textbody
		titleShape.setTxBody(ctTB);
		// add shape
		slide.getJaxbElement().getCSld().getSpTree().getSpOrGrpSpOrGraphicFrame().add(titleShape);
	}

	/**
	 * Adds content to a slide's first content area.
	 *
	 * @param slide the slide to add data to
	 * @param content the data to add to the content 1 area
	 */
	private void addContent1(SlidePart slide, ArrayList<String> content){
		// create a new shape and set its id and name
		Shape standardShape = createShape(3, "Content Placeholder 2", STPlaceholderType.BODY, 1);

		// create SpPr
		standardShape.setSpPr(new CTShapeProperties());
		CTTextBody ctTB = createTextBody(content);
		//ctTB.getP().get(0).getPPr().setAlgn(STTextAlignType.CTR);
		ctTB.getP().get(0).getPPr().setLvl(0);
		// add textbody
		standardShape.setTxBody(ctTB);
		// add shape
		slide.getJaxbElement().getCSld().getSpTree().getSpOrGrpSpOrGraphicFrame().add(standardShape);
	}

	/**
	 * Adds content to a slide's second content area.
	 *
	 * @param slide the slide to add data to
	 * @param content the data to add to the content 2 area
	 */
	private void addContent2(SlidePart slide, ArrayList<String> content){
		// create a new shape and set its id and name
		Shape standardShape = createShape(4, "Content Placeholder 3", STPlaceholderType.BODY, 2);

		// create SpPr
		standardShape.setSpPr(new CTShapeProperties());
		CTTextBody ctTB = createTextBody(content);
		//ctTB.getP().get(0).getPPr().setAlgn(STTextAlignType.CTR);
		//ctTP.getPPr().setAlgn(STTextAlignType.CTR);
		// add textbody
		standardShape.setTxBody(ctTB);
		// add shape
		slide.getJaxbElement().getCSld().getSpTree().getSpOrGrpSpOrGraphicFrame().add(standardShape);
	}

	/**
	 * Creates a CTTextBody object for adding to a slide. Adds filler data
	 * when an area is empty because if there is nothing in the output pptx
	 * file there is no way to edit the content area.
	 *
	 * @param paras the paragraphs to add to the CTTextBody
	 * @return the created CTTextBody
	 */
	private CTTextBody createTextBody(ArrayList<String> paras) {
		CTTextBody ctTB = new CTTextBody();
		ctTB.setBodyPr(new CTTextBodyProperties());
		//ctTB.getBodyPr().setRtlCol(Boolean.TRUE);
		//ctTB.getBodyPr().setAnchor(STTextAnchoringType.CTR);
		ctTB.setLstStyle(new CTTextListStyle());
		// create P
		List<CTTextParagraph> p = ctTB.getP();
		if(paras.isEmpty()){
			paras = new ArrayList<String>();
			paras.add("(Add Content)");
		}
		for(String str : paras){
			p.add(createParagraph(str));
		}
		return ctTB;
	}

	/**
	 * Creates a new CTTextParagraph for adding to a CTTextBody.
	 *
	 * @param para the string to add, each string becomes a paragraph
	 * @return the created CTTextParagraph
	 */
	private CTTextParagraph createParagraph(String para) {
		CTTextParagraph ctTP = new CTTextParagraph();
		ctTP.setPPr(new CTTextParagraphProperties());
		ctTP.getEGTextRun().add(createRun(para));
		ctTP.setEndParaRPr(new CTTextCharacterProperties());
		ctTP.getEndParaRPr().setLang("en-US");
		//ctTP.getEndParaRPr().setDirty(Boolean.FALSE);
		return ctTP;
	}

	/**
	 * Creates a CTRegularTextRun for adding to a CTTextParaph. Each String
	 * is a sentence. Using different Runs can produce bold and italics.
	 *
	 * @param str the string to add to the run
	 * @return the created CTRegularTextRun
	 */
	private CTRegularTextRun createRun(String str) {
		CTRegularTextRun ctRTR = new CTRegularTextRun();
		ctRTR.setRPr(new CTTextCharacterProperties());
		ctRTR.getRPr().setLang("en-US");
		ctRTR.getRPr().setDirty(Boolean.FALSE);
		ctRTR.getRPr().setSmtClean(Boolean.FALSE);
		ctRTR.setT(str);
		return ctRTR;
	}

	/**
	 * Creates a new Shape object. In pptx/odp these are the basis for all
	 * objects. Shapes can become text boxes or actual shapes, such as circles,
	 * squares, etc.
	 *
	 * @param id determines how the shape is formatted according to the layout
	 * @param name also relates to the layout file
	 * @param type sets extra attributes if the shape is a title
	 * @param idx used to format the shape further
	 * @return a new Shape object
	 */
	private Shape createShape(int id, String name, STPlaceholderType type, int idx){
		Shape scratch = new Shape();
		// create NvSpPr, set attribs
		scratch.setNvSpPr(new NvSpPr());{
			scratch.getNvSpPr().setCNvPr(new CTNonVisualDrawingProps());
			scratch.getNvSpPr().getCNvPr().setId(id);
			scratch.getNvSpPr().getCNvPr().setName(name);
			scratch.getNvSpPr().setCNvSpPr(new CTNonVisualDrawingShapeProps());
			scratch.getNvSpPr().getCNvSpPr().setSpLocks(new CTShapeLocking());
			scratch.getNvSpPr().getCNvSpPr().getSpLocks().setNoGrp(Boolean.TRUE);}
		// create NvPr, set attribs
		scratch.getNvSpPr().setNvPr(new NvPr());{
			scratch.getNvSpPr().getNvPr().setPh(new CTPlaceholder());
			if(type == STPlaceholderType.TITLE || type == STPlaceholderType.CTR_TITLE)
				scratch.getNvSpPr().getNvPr().getPh().setType(type);
			if(idx>0)
				scratch.getNvSpPr().getNvPr().getPh().setIdx(Long.valueOf(idx));}
		return scratch;
	}

	/**
	 * Saves the presentation created in the application. Takes in the array of
	 * SlideData objects and creates corresponding SlideParts to write them out.
	 *
	 * @param saveFile the file to save to
	 * @param slides the SlideData objects to convert
	 * @return true if the save was successful
	 */
	public boolean savePresentation(File saveFile, ArrayList<SlideData> slides) {
		if(saveFile != null){
			System.out.println("PC> creating powerpoint");
			SlidePart slide = null;
			ArrayList<String> data;
			// loop through all slides
			for(SlideData sd : slides){
				// create correct type of slide
				switch(sd.getType()){
					case Globals.sType_title:
						slide = this.createTitleSlide();
						break;
					case Globals.sType_standard:
						slide = this.createStandardSlide();
						break;
					case Globals.sType_two_content:
						slide = this.createTwoContentSlide();
						break;
				}
				// TODO expand to write out list styles, smartart, & tagging
				// title data
				data = sd.getTitle().getContent();
				if(sd.getTitle().getSmartArt()>0)
					data.add(0,tagForSmartArt(sd.getTitle()));
				this.addTitle(slide, data);
				data = sd.getContent1().getContent();
				if(sd.getContent1().getSmartArt()>0)
					data.add(0, tagForSmartArt(sd.getContent1()));
				this.addContent1(slide, data);
				if(sd.getType()==Globals.sType_two_content){
					data = sd.getContent2().getContent();
					if(sd.getContent2().getSmartArt()>0)
						data.add(0,tagForSmartArt(sd.getContent2()));
					this.addContent2(slide, sd.getContent2().getContent());
				}
			}

			return write(saveFile.toString());
		}
		return false;
	}

	/**
	 * Inserts a reminder for the selected smartart style, allowing it to be
	 * written to the content area in front of the other data.
	 *
	 * @param sds the SlideDataSection to get its smartart style
	 * @return the string to add for the selected smartart
	 */
	private String tagForSmartArt(SlideDataSection sds){
		String saStyle = "<Tagged for SmartArt style: ";
		switch(sds.getSmartArt()){
			case Globals.saStyle_process:
				saStyle += "Process>";
				break;
			case Globals.saStyle_hierarchy:
				saStyle += "Hierarchy>";
				break;
			case Globals.saStyle_pyramid:
				saStyle += "Pyramid>";
				break;
			case Globals.saStyle_cycle:
				saStyle += "Cycle>";
				break;
		}
		return saStyle;
	}

	/**
	 * Used to write out the created presentation file.
	 *
	 * @param outputFile the file to write to
	 * @return true if the write was successful
	 */
	private boolean write(String outputFile){
		System.out.println("PC> writing file: "+outputFile);
		boolean success = false;
		// TODO fix this to read correct path
		boolean save = (outputFile == null ? false : true);
		// Now save it
		if (save) {
			try {
				presMLPackage.save(new File(outputFile));
				success = true;
				System.out.println("PC> Success! Saved " + outputFile);
			} catch (Docx4JException e) {
				System.err.println("Error: PC> Save failed");
			}
		} else {
			try{
				// Create a org.docx4j.wml.Package object
				FlatOpcXmlCreator worker = new FlatOpcXmlCreator(presMLPackage);
				org.docx4j.xmlPackage.Package pkg = worker.get();
				// Now marshall it
				JAXBContext jc = Context.jcPML;
				Marshaller marshaller = jc.createMarshaller();
				marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
				NamespacePrefixMapperUtils.setProperty(marshaller, 
						NamespacePrefixMapperUtils.getPrefixMapper());
				System.out.println( "\n\n OUTPUT " );
				System.out.println( "====== \n\n " );
				marshaller.marshal(pkg, System.out);
				success = true;
				System.out.println("PC> Success! Saved " + outputFile);
			} catch (Docx4JException e) {
				System.err.println("Error: PC> Docx4J failure");
			} catch (JAXBException e) {
				// TODO make custom debug class to print errors
				e.getStackTrace()[0].getLineNumber();
				System.err.println("Error: PC> JAXB failed");
			}
			
		}
		return success;
	}
}
